﻿using Lucene.Net.Analysis;
using Lucene.Net.Analysis.CJK;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using Lucene.Net.Store;
using Lucene.Net.Store.Azure;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CloudStorageLight.Core
{
    /// <summary>
    /// 全文検索サービス
    /// </summary>
    public class FullTextServiceBase : IDisposable
    {

        private string catalogName;
        private string connectionString;

        private string poolKey { get { return connectionString + "_" + catalogName; } }

        private static Dictionary<string, IndexWriter> indexWriterPool = new Dictionary<string, IndexWriter>();
        private static ConcurrentDictionary<string, IndexSearcher> IndexSearcherPool = new ConcurrentDictionary<string, IndexSearcher>();

        private IndexWriter indexWriter 
        {
            get
            {
                return indexWriterPool[poolKey];
            }
        }
        //private static object indexWriterLockObject = new object();

        private IndexSearcher searcher
        {
            get
            {
                return IndexSearcherPool[poolKey];
            }
        }
        //private static object searcherLockObject = new object();

        private static int RetryCount = 3;


        static FullTextServiceBase()
        {

        }

        public FullTextServiceBase(string catalogName, string connectionString)
        {
            this.catalogName = catalogName;
            this.connectionString = connectionString;
            InitIfNotExist();
        }

        public virtual void Init()
        {
            foreach (var key in indexWriterPool.Keys.ToList())
            {
                indexWriterPool[key].Dispose();
                indexWriterPool.Remove(key);
            }
            foreach (var key in IndexSearcherPool.Keys.ToList())
            {
                IndexSearcherPool[key].Dispose();
                IndexSearcher outWork;
                if (!IndexSearcherPool.TryRemove(key, out outWork)) throw new InvalidOperationException();

            }

            var storageAccount = CloudStorageAccount.Parse(connectionString);
            var azureDirectory = GetAzureDirectory(storageAccount, catalogName);
            azureDirectory.ClearLock("write.lock");
            using (var indexWriter = new IndexWriter(azureDirectory, CreateAnalyzer("Init"), true, new Lucene.Net.Index.IndexWriter.MaxFieldLength(IndexWriter.DEFAULT_MAX_FIELD_LENGTH)))
            {
            }
        }

        private AzureDirectory GetAzureDirectory(CloudStorageAccount storageAccount, string connectionString)
        {
            if (_azureDirectory == null)
            {
                if (SystemSettings.Instance.UseRAMDirectory)
                {
                    _azureDirectory = new AzureDirectory(storageAccount, catalogName, new RAMDirectory(), false, null, SystemSettings.Instance.BlobEncryptKey);
                }
                else
                {
                    _azureDirectory = new AzureDirectory(storageAccount, catalogName,null,false,null, SystemSettings.Instance.BlobEncryptKey);
                }
            }
            return _azureDirectory;

        }
        private AzureDirectory _azureDirectory;


        public virtual void InitIfNotExist()
        {
            var storageAccount = CloudStorageAccount.Parse(connectionString);
            var blobClient = storageAccount.CreateCloudBlobClient();
            var fileContainer = blobClient.GetContainerReference(catalogName);
            if (!fileContainer.Exists())
            {
                var azureDirectory = GetAzureDirectory(storageAccount, catalogName);
                using (var indexWriter = new IndexWriter(azureDirectory, CreateAnalyzer("Init"), true, new Lucene.Net.Index.IndexWriter.MaxFieldLength(IndexWriter.DEFAULT_MAX_FIELD_LENGTH)))
                {
                }            
            }
        }

        public async virtual Task Entry(string id, string title, string content, string owner = null)
        {
            for (int i = 1; i <= RetryCount; i++ )
            {
                Debug.WriteLine("FullTextServiceBase.Entry:" + id);
                try
                {
                    EnsureIndexWrite();

                    Term idTerm = new Term("Id", id);
                    Document doc = new Document();
                    doc.Add(new Field("Id", id, Field.Store.YES, Field.Index.NOT_ANALYZED, Field.TermVector.NO));
                    doc.Add(new Field("Title", title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.YES));
                    if (content != null)
                    {
                        doc.Add(new Field("Body", content, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.YES));
                    }
                    if (owner != null) doc.Add(new Field("Owner", owner, Field.Store.YES, Field.Index.NOT_ANALYZED, Field.TermVector.YES));
                    indexWriter.UpdateDocument(idTerm, doc, CreateAnalyzer("UpdateDocument"));
                    return;
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.ToString());
                    try
                    {
                        CloseIndexWriter();
                    }
                    catch { }
                    if (RetryCount == i + 1) throw;
                }
                var tim = i * i * 3000;
                await Task.Delay(tim);

            }
        }
        public async virtual Task DeleteEntry(string id)
        {
            for (int i = 1; i <= RetryCount; i++)
            {
                Debug.WriteLine("FullTextServiceBase.DeleteEntry:" + id);

                try
                {
                    EnsureIndexWrite();

                    Term idTerm = new Term("Id", id);
                    indexWriter.DeleteDocuments(idTerm);
                    Debug.WriteLine("FullTextService DeleteEntry:" + id);
                    return;
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.ToString());
                    try
                    {
                        CloseIndexWriter();
                    }
                    catch { }
                    if (RetryCount == i + 1) throw;
                }
                var tim = i * i * 3000;
                await Task.Delay(tim);
            }
        }


        protected virtual Analyzer CreateAnalyzer(string mode)
        {
            return new CJKAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
        }

        private  void EnsureIndexWrite()
        {
            if (!indexWriterPool.ContainsKey(poolKey))
            {
                lock (indexWriterPool)
                {
                    if (!indexWriterPool.ContainsKey(poolKey))
                    {
                        var storageAccount = CloudStorageAccount.Parse(connectionString);
                        var azureDirectory = GetAzureDirectory(storageAccount, catalogName);
                        indexWriterPool[poolKey] = new IndexWriter(azureDirectory, CreateAnalyzer("IndexWriter"), false, new Lucene.Net.Index.IndexWriter.MaxFieldLength(IndexWriter.DEFAULT_MAX_FIELD_LENGTH));
                    }
                }
            }
        }

        private void EnsureSearcherIndex()
        {
            if (!IndexSearcherPool.ContainsKey(poolKey))
            {
                IndexSearcherPool[poolKey] = CreateIndexSearcher();

            }
        }

        private IndexSearcher CreateIndexSearcher()
        {
            var storageAccount = CloudStorageAccount.Parse(connectionString);
            var azureDirectory = GetAzureDirectory(storageAccount, catalogName);
            return new IndexSearcher(azureDirectory, true);
        }
        private void CloseIndexWriter()
        {
            if (indexWriter != null)
            {
                indexWriter.Dispose(false);
                indexWriterPool.Remove(poolKey);
            }
        }

        //必ず\から置き換る
        private static string[] queryEscapeChars = "\\ + - && || ! ( ) { } [ ] ^ \" ~ * ? :".Split(' ');
 
        public virtual List<FullTextSearchResult> Search(string query, int resultSize = 10)
        {
            query = EscapeQuery(query);
            return SearchByRawQuery("Body:(" + query + ") OR Title:(" + query +") OR Owner:(" + query + ")", resultSize);
        }

        public string EscapeQuery(string query)
        {
            foreach (var escapeChar in queryEscapeChars)
            {
                query = query.Replace(escapeChar, "\\" + escapeChar);
            }
            return query;
        }


        public virtual FullTextSearchResult SearchById(string id)
        {
            if (string.IsNullOrEmpty(id)) throw new ArgumentNullException();

            Debug.WriteLine("FullTextServiceBase.SearchById:" + id);

            EnsureSearcherIndex();
            var q = new TermQuery(new Term("Id", id));
            var hits = searcher.Search(q, 1);
            if (hits.ScoreDocs.Count() == 0) return null;
            var targetDoc = hits.ScoreDocs[0];
            var title = searcher.Doc(targetDoc.Doc).GetField("Title").StringValue;
            var contentField = searcher.Doc(targetDoc.Doc).GetField("Body");
            var content = "";
            if (contentField != null)
            {
                content = contentField.StringValue;
            }
            return new FullTextSearchResult(id, title, content, targetDoc.Score);
        }




        /// <summary>
        /// 検索
        /// </summary>
        /// <param name="query"></param>
        /// <param name="resultSize"></param>
        /// <returns></returns>
        public virtual List<FullTextSearchResult> SearchByRawQuery(string query, int resultSize = 10)
        {
            if (string.IsNullOrEmpty(query)) throw new ArgumentNullException();

            Debug.WriteLine("FullTextServiceBase.SearchByRawQuery:" + query);

            var results = new List<FullTextSearchResult>();

            EnsureSearcherIndex();
            Lucene.Net.QueryParsers.QueryParser parser = new QueryParser(Lucene.Net.Util.Version.LUCENE_30, "Title", CreateAnalyzer("Search"));
            Lucene.Net.Search.Query q = parser.Parse(query);
            var hits = searcher.Search(q, resultSize);
            for (int i = 0; i < hits.ScoreDocs.Count(); i++)
            {
                var targetDoc = hits.ScoreDocs[i];
                var id = searcher.Doc(targetDoc.Doc).GetField("Id").StringValue;
                var title = searcher.Doc(targetDoc.Doc).GetField("Title").StringValue;
                var contentField = searcher.Doc(targetDoc.Doc).GetField("Body");
                var content = "";
                if (contentField != null)
                {
                    content = contentField.StringValue;
                }

                results.Add(new FullTextSearchResult(id, title, content, targetDoc.Score));
            }
            return results;
        }

        public void RenewSearcherIndex()
        {
            if (!IndexSearcherPool.ContainsKey(poolKey))
            {
                IndexSearcherPool[poolKey] = CreateIndexSearcher();

            }
            else
            {
                var old = IndexSearcherPool[poolKey];
                IndexSearcherPool[poolKey] = CreateIndexSearcher();
                old.Dispose();
            }
        }

        /// <summary>
        /// 全文検索のコミット
        /// </summary>
        public void Commit()
        {
            EnsureIndexWrite();
            indexWriter.Commit();
        }

        /// <summary>
        /// 検索インデックスの最適化
        /// </summary>
        public void Optimize()
        {
            EnsureIndexWrite();
            indexWriter.Optimize();
        }
        bool disposed = false;

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposed) return;
            if (disposing)
            {
                //Debug.WriteLine("FullTextServiceBase.Dispose:");

                //if (indexWriter != null)
                //{
                //    indexWriter.Dispose(false);
                //    indexWriter = null;
                //}
                //if (searcher != null)
                //{
                //    //searcher.Dispose();
                //    //searcher = null;
                //}
            }

            disposed = true;
        }
    }
}
